﻿//------------------------------------------------------------------------------
// <copyright file="FormsAuthenticationTicketSerializer.cs" company="Microsoft">
//     Copyright (c) Microsoft Corporation.  All rights reserved.
// </copyright>
//------------------------------------------------------------------------------

namespace System.Web.Security {
    using System;
    using System.IO;
    using System.Security.Cryptography;
    using System.Text;
    using System.Web.Util;

    // A helper class which can serialize / deserialize FormsAuthenticationTicket instances.
    //
    // MSRC 11838 / DevDiv #292994 (http://vstfdevdiv:8080/DevDiv2/web/wi.aspx?id=292994):
    // We need to fix the format of the serialized FormsAuthenticationTicket to account for
    // the fact that the string payloads can contain any arbitrary characters, including
    // embedded nulls. In particular, because of that vulnerability, we must assume that *any*
    // FormsAuthenticationTicket generated by a pre-patch system is potentially the result
    // of a malicious action. This new serialized format was chosen because it guarantees
    // a compatibility break between either old format and the new format: pre-patch systems
    // will reject post-patch tickets as having an invalid format, and post-patch systems
    // will also reject pre-patch tickets as having an invalid format.

    /* Current (v1) ticket format
     * ==========================
     * 
     * Serialized ticket format version number: 1 byte
     * FormsAuthenticationTicket.Version: 1 byte
     * FormsAuthenticationTicket.IssueDateUtc: 8 bytes
     * {spacer}: 1 byte
     * FormsAuthenticationTicket.ExpirationUtc: 8 bytes
     * FormsAuthenticationTicket.IsPersistent: 1 byte
     * FormsAuthenticationTicket.Name: 1+ bytes (1+ length prefix, 0+ payload)
     * FormsAuthenticationTicket.UserData: 1+ bytes (1+ length prefix, 0+ payload)
     * FormsAuthenticationTicket.CookiePath: 1+ bytes (1+ length prefix, 0+ payload)
     * {footer}: 1 byte
     */

    internal static class FormsAuthenticationTicketSerializer {

        private const byte CURRENT_TICKET_SERIALIZED_VERSION = 0x01;

        // Resurrects a FormsAuthenticationTicket from its serialized blob representation.
        // The input blob must be unsigned and unencrypted. This function returns null if
        // the serialized ticket format is invalid. The caller must also verify that the
        // ticket is still valid, as this method doesn't check expiration.
        public static FormsAuthenticationTicket Deserialize(byte[] serializedTicket, int serializedTicketLength) {
            try {
                using (MemoryStream ticketBlobStream = new MemoryStream(serializedTicket)) {
                    using (SerializingBinaryReader ticketReader = new SerializingBinaryReader(ticketBlobStream)) {

                        // Step 1: Read the serialized format version number from the stream.
                        // Currently the only supported format is 0x01.
                        // LENGTH: 1 byte
                        byte serializedFormatVersion = ticketReader.ReadByte();
                        if (serializedFormatVersion != CURRENT_TICKET_SERIALIZED_VERSION) {
                            return null; // unexpected value
                        }

                        // Step 2: Read the ticket version number from the stream.
                        // LENGTH: 1 byte
                        int ticketVersion = ticketReader.ReadByte();

                        // Step 3: Read the ticket issue date from the stream.
                        // LENGTH: 8 bytes
                        long ticketIssueDateUtcTicks = ticketReader.ReadInt64();
                        DateTime ticketIssueDateUtc = new DateTime(ticketIssueDateUtcTicks, DateTimeKind.Utc);
                        DateTime ticketIssueDateLocal = ticketIssueDateUtc.ToLocalTime();

                        // Step 4: Read the spacer from the stream.
                        // LENGTH: 1 byte
                        byte spacer = ticketReader.ReadByte();
                        if (spacer != 0xfe) {
                            return null; // unexpected value
                        }

                        // Step 5: Read the ticket expiration date from the stream.
                        // LENGTH: 8 bytes
                        long ticketExpirationDateUtcTicks = ticketReader.ReadInt64();
                        DateTime ticketExpirationDateUtc = new DateTime(ticketExpirationDateUtcTicks, DateTimeKind.Utc);
                        DateTime ticketExpirationDateLocal = ticketExpirationDateUtc.ToLocalTime();

                        // Step 6: Read the ticket persistence field from the stream.
                        // LENGTH: 1 byte
                        byte ticketPersistenceFieldValue = ticketReader.ReadByte();
                        bool ticketIsPersistent;
                        switch (ticketPersistenceFieldValue) {
                            case 0:
                                ticketIsPersistent = false;
                                break;
                            case 1:
                                ticketIsPersistent = true;
                                break;
                            default:
                                return null; // unexpected value
                        }

                        // Step 7: Read the ticket username from the stream.
                        // LENGTH: 1+ bytes (7-bit encoded integer char count + UTF-16LE payload)
                        string ticketName = ticketReader.ReadBinaryString();

                        // Step 8: Read the ticket custom data from the stream.
                        // LENGTH: 1+ bytes (7-bit encoded integer char count + UTF-16LE payload)
                        string ticketUserData = ticketReader.ReadBinaryString();

                        // Step 9: Read the ticket cookie path from the stream.
                        // LENGTH: 1+ bytes (7-bit encoded integer char count + UTF-16LE payload)
                        string ticketCookiePath = ticketReader.ReadBinaryString();

                        // Step 10: Read the footer from the stream.
                        // LENGTH: 1 byte
                        byte footer = ticketReader.ReadByte();
                        if (footer != 0xff) {
                            return null; // unexpected value
                        }

                        // Step 11: Verify that we have consumed the entire payload.
                        // We don't expect there to be any more information after the footer.
                        // The caller is responsible for telling us when the actual payload
                        // is finished, as he may have handed us a byte array that contains
                        // the payload plus signature as an optimization, and we don't want
                        // to misinterpet the signature as a continuation of the payload.
                        if (ticketBlobStream.Position != serializedTicketLength) {
                            return null;
                        }

                        // Success.
                        return FormsAuthenticationTicket.FromUtc(
                            ticketVersion /* version */,
                            ticketName /* name */,
                            ticketIssueDateUtc /* issueDateUtc */,
                            ticketExpirationDateUtc /* expirationUtc */,
                            ticketIsPersistent /* isPersistent */,
                            ticketUserData /* userData */,
                            ticketCookiePath /* cookiePath */);
                    }
                }
            }
            catch {
                // If anything goes wrong while parsing the token, just treat the token as invalid.
                return null;
            }
        }

        // Turns a FormsAuthenticationTicket into a serialized blob.
        // The resulting blob is not encrypted or signed.
        public static byte[] Serialize(FormsAuthenticationTicket ticket) {
            using (MemoryStream ticketBlobStream = new MemoryStream()) {
                using (SerializingBinaryWriter ticketWriter = new SerializingBinaryWriter(ticketBlobStream)) {

                    // SECURITY NOTE:
                    // Earlier versions of the serializer (Framework20 / Framework40) wrote out a
                    // random 8-byte header as the first part of the payload. This random header
                    // was used as an IV when the ticket was encrypted, since the early encryption
                    // routines didn't automatically append an IV when encrypting data. However,
                    // the MSRC 10405 (Pythia) patch causes all of our crypto routines to use an
                    // IV automatically, so there's no need for us to include a random IV in the
                    // serialized stream any longer. We can just write out only the data, and the
                    // crypto routines will do the right thing.

                    // Step 1: Write the ticket serialized format version number (currently 0x01) to the stream.
                    // LENGTH: 1 byte
                    ticketWriter.Write(CURRENT_TICKET_SERIALIZED_VERSION);

                    // Step 2: Write the ticket version number to the stream.
                    // This is the developer-specified FormsAuthenticationTicket.Version property,
                    // which is just ticket metadata. Technically it should be stored as a 32-bit
                    // integer instead of just a byte, but we have historically been storing it
                    // as just a single byte forever and nobody has complained.
                    // LENGTH: 1 byte
                    ticketWriter.Write((byte)ticket.Version);

                    // Step 3: Write the ticket issue date to the stream.
                    // We store this value as UTC ticks. We can't use DateTime.ToBinary() since it
                    // isn't compatible with .NET v1.1.
                    // LENGTH: 8 bytes (64-bit little-endian in payload)
                    ticketWriter.Write(ticket.IssueDateUtc.Ticks);

                    // Step 4: Write a one-byte spacer (0xfe) to the stream.
                    // One of the old ticket formats (Framework40) expects the unencrypted payload
                    // to contain 0x000000 (3 null bytes) beginning at position 9 in the stream.
                    // Since we're currently at offset 10 in the serialized stream, we can take
                    // this opportunity to purposely inject a non-null byte at this offset, which
                    // intentionally breaks compatibility with Framework40 mode.
                    // LENGTH: 1 byte
                    Debug.Assert(ticketBlobStream.Position == 10, "Critical that we be at position 10 in the stream at this point.");
                    ticketWriter.Write((byte)0xfe);

                    // Step 5: Write the ticket expiration date to the stream.
                    // We store this value as UTC ticks.
                    // LENGTH: 8 bytes (64-bit little endian in payload)
                    ticketWriter.Write(ticket.ExpirationUtc.Ticks);

                    // Step 6: Write the ticket persistence field to the stream.
                    // LENGTH: 1 byte
                    ticketWriter.Write(ticket.IsPersistent);

                    // Step 7: Write the ticket username to the stream.
                    // LENGTH: 1+ bytes (7-bit encoded integer char count + UTF-16LE payload)
                    ticketWriter.WriteBinaryString(ticket.Name);

                    // Step 8: Write the ticket custom data to the stream.
                    // LENGTH: 1+ bytes (7-bit encoded integer char count + UTF-16LE payload)
                    ticketWriter.WriteBinaryString(ticket.UserData);

                    // Step 9: Write the ticket cookie path to the stream.
                    // LENGTH: 1+ bytes (7-bit encoded integer char count + UTF-16LE payload)
                    ticketWriter.WriteBinaryString(ticket.CookiePath);

                    // Step 10: Write a one-byte footer (0xff) to the stream.
                    // One of the old FormsAuthenticationTicket formats (Framework20) requires
                    // that the payload end in 0x0000 (U+0000). By making the very last byte
                    // of this format non-null, we can guarantee a compatiblity break between
                    // this format and Framework20.
                    // LENGTH: 1 byte
                    ticketWriter.Write((byte)0xff);

                    // Finished.
                    return ticketBlobStream.ToArray();
                }
            }
        }

        // see comments on SerializingBinaryWriter
        private sealed class SerializingBinaryReader : BinaryReader {
            public SerializingBinaryReader(Stream input)
                : base(input) {
            }

            public string ReadBinaryString() {
                int charCount = Read7BitEncodedInt();
                byte[] bytes = ReadBytes(charCount * 2);

                char[] chars = new char[charCount];
                for (int i = 0; i < chars.Length; i++) {
                    chars[i] = (char)(bytes[2 * i] | (bytes[2 * i + 1] << 8));
                }

                return new String(chars);
            }

            public override string ReadString() {
                // should never call this method since it will produce wrong results
                throw new NotImplementedException();
            }
        }

        // This is a special BinaryWriter which serializes strings in a way that is
        // entirely round-trippable. For example, the string "\ud800" is a valid .NET
        // Framework string, but since U+D800 is an unpaired Unicode surrogate the
        // built-in Encoding types will not round-trip it. Strings are serialized as a
        // 7-bit character count (not byte count!) followed by a UTF-16LE payload.
        private sealed class SerializingBinaryWriter : BinaryWriter {
            public SerializingBinaryWriter(Stream output)
                : base(output) {
            }

            public override void Write(string value) {
                // should never call this method since it will produce wrong results
                throw new NotImplementedException();
            }

            public void WriteBinaryString(string value) {
                byte[] bytes = new byte[value.Length * 2];
                for (int i = 0; i < value.Length; i++) {
                    char c = value[i];
                    bytes[2 * i] = (byte)c;
                    bytes[2 * i + 1] = (byte)(c >> 8);
                }

                Write7BitEncodedInt(value.Length);
                Write(bytes);
            }
        }

    }
}
